#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import re
import sys
import gzip
import json
import base64
import random
import urllib
import logging
import urlparse
import posixpath
import cStringIO
import BaseHTTPServer
import SimpleHTTPServer
import SocketServer
import socket
import settings
from urls import urls
from utils.context import ctx

logger = logging.getLogger('appserver')

urls = [(re.compile(pattern), _) for pattern, _ in urls]
error_page = 'PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KPG1ldGEgY2hhcnNldD0idXRmLTgiPgo8bWV0YSBodHRwLWVxdWl2PSJ4LXVhLWNvbXBhdGlibGUiIGNvbnRlbnQ9ImllPWVkZ2UiPgo8dGl0bGU+JXM8L3RpdGxlPgo8c3R5bGU+CiAgICBib2R5IHsKICAgICAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjRUNFQ0VDOwogICAgICAgIGZvbnQtZmFtaWx5OiAnT3BlbiBTYW5zJywgc2Fucy1zZXJpZjsKICAgICAgICBmb250LXNpemU6IDE0cHg7CiAgICAgICAgY29sb3I6ICMzYzNjM2M7CiAgICAgICAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTsKICAgICAgICAtbW96LXVzZXItc2VsZWN0OiBub25lOwogICAgICAgIC1tcy11c2VyLXNlbGVjdDogbm9uZTsKICAgIH0KCiAgICAuZGVtbyBwOmZpcnN0LWNoaWxkIHsKICAgICAgICB0ZXh0LWFsaWduOiBjZW50ZXI7CiAgICAgICAgZm9udC1mYW1pbHk6IGN1cnNpdmU7CiAgICAgICAgZm9udC1zaXplOiAxNTBweDsKICAgICAgICBmb250LXdlaWdodDogYm9sZDsKICAgICAgICBsaW5lLWhlaWdodDogMTAwcHg7CiAgICAgICAgbGV0dGVyLXNwYWNpbmc6IDVweDsKICAgICAgICBjb2xvcjogI2ZmZjsKICAgIH0KCiAgICAuZGVtbyBwOmZpcnN0LWNoaWxkIHNwYW4gewogICAgICAgIGN1cnNvcjogcG9pbnRlcjsKICAgICAgICB0ZXh0LXNoYWRvdzogMHB4IDBweCAycHggIzY4Njg2OCwKICAgICAgICAgICAgMHB4IDFweCAxcHggI2RkZCwKICAgICAgICAgICAgMHB4IDJweCAxcHggI2Q2ZDZkNiwKICAgICAgICAgICAgMHB4IDNweCAxcHggI2NjYywKICAgICAgICAgICAgMHB4IDRweCAxcHggI2M1YzVjNSwKICAgICAgICAgICAgMHB4IDVweCAxcHggI2MxYzFjMSwKICAgICAgICAgICAgMHB4IDZweCAxcHggI2JiYiwKICAgICAgICAgICAgMHB4IDdweCAxcHggIzc3NywKICAgICAgICAgICAgMHB4IDhweCAzcHggcmdiYSgxMDAsIDEwMCwgMTAwLCAwLjQpLAogICAgICAgICAgICAwcHggOXB4IDVweCByZ2JhKDEwMCwgMTAwLCAxMDAsIDAuMSksCiAgICAgICAgICAgIDBweCAxMHB4IDdweCByZ2JhKDEwMCwgMTAwLCAxMDAsIDAuMTUpLAogICAgICAgICAgICAwcHggMTFweCA5cHggcmdiYSgxMDAsIDEwMCwgMTAwLCAwLjIpLAogICAgICAgICAgICAwcHggMTJweCAxMXB4IHJnYmEoMTAwLCAxMDAsIDEwMCwgMC4yNSksCiAgICAgICAgICAgIDBweCAxM3B4IDE1cHggcmdiYSgxMDAsIDEwMCwgMTAwLCAwLjMpOwogICAgICAgIC13ZWJraXQtdHJhbnNpdGlvbjogYWxsIC4xcyBsaW5lYXI7CiAgICAgICAgdHJhbnNpdGlvbjogYWxsIC4xcyBsaW5lYXI7CiAgICB9CgogICAgLmRlbW8gcDpmaXJzdC1jaGlsZCBzcGFuOmhvdmVyIHsKICAgICAgICB0ZXh0LXNoYWRvdzogMHB4IDBweCAycHggIzY4Njg2OCwKICAgICAgICAgICAgMHB4IDFweCAxcHggI2ZmZiwKICAgICAgICAgICAgMHB4IDJweCAxcHggI2ZmZiwKICAgICAgICAgICAgMHB4IDNweCAxcHggI2ZmZiwKICAgICAgICAgICAgMHB4IDRweCAxcHggI2ZmZiwKICAgICAgICAgICAgMHB4IDVweCAxcHggI2ZmZiwKICAgICAgICAgICAgMHB4IDZweCAxcHggI2ZmZiwKICAgICAgICAgICAgMHB4IDdweCAxcHggIzc3NywKICAgICAgICAgICAgMHB4IDhweCAzcHggI2ZmZiwKICAgICAgICAgICAgMHB4IDlweCA1cHggI2ZmZiwKICAgICAgICAgICAgMHB4IDEwcHggN3B4ICNmZmYsCiAgICAgICAgICAgIDBweCAxMXB4IDlweCAjZmZmLAogICAgICAgICAgICAwcHggMTJweCAxMXB4ICNmZmYsCiAgICAgICAgICAgIDBweCAxM3B4IDE1cHggI2ZmZjsKICAgICAgICAtd2Via2l0LXRyYW5zaXRpb246IGFsbCAuMXMgbGluZWFyOwogICAgICAgIHRyYW5zaXRpb246IGFsbCAuMXMgbGluZWFyOwogICAgfQoKICAgIC5kZW1vIHA6bm90KDpmaXJzdC1jaGlsZCkgewogICAgICAgIHRleHQtYWxpZ246IGNlbnRlcjsKICAgICAgICBjb2xvcjogIzY2NjsKICAgICAgICBmb250LWZhbWlseTogY3Vyc2l2ZTsKICAgICAgICBmb250LXNpemU6IDIwcHg7CiAgICAgICAgdGV4dC1zaGFkb3c6IDAgMXB4IDAgI2ZmZjsKICAgICAgICBsZXR0ZXItc3BhY2luZzogMXB4OwogICAgICAgIGxpbmUtaGVpZ2h0OiAyZW07CiAgICAgICAgbWFyZ2luLXRvcDogLTUwcHg7CiAgICB9Cjwvc3R5bGU+CjwvaGVhZD4KCjxib2R5PgogICAgPGRpdiBjbGFzcz0iZGVtbyI+CiAgICAgICAgPHA+JXM8L3A+CiAgICAgICAgPHA+JXM8L3A+CiAgICA8L2Rpdj4KPC9ib2R5Pgo8L2h0bWw+Cg=='
error_page = base64.b64decode(error_page)

g_content = {
    403: error_page % ('403', '<span>%s</span><span>%s</span><span>%s</span>' % tuple('403'), 'Forbidden'),
    404: error_page % ('404', '<span>%s</span><span>%s</span><span>%s</span>' % tuple('404'), 'Not Found'),
    500: error_page % ('500', '<span>%s</span><span>%s</span><span>%s</span>' % tuple('500'), 'Internal Server Error')
}


class TimeoutHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):

    timeout = 60
    protocol_version = 'HTTP/1.1'
    htdocs_path = settings.DOCUMENT_DIR
    server_names = [
        'Apache/2.2.4 (FreeBSD)',
        'CommuniGatePro/5.1.12',
        'IBM HTTP Server/V5R3M0',
        'LiteSpeed/5.0 (Solaris)',
        'Microsoft-IIS/8.5',
        'Sun-ONE-Web-Server/6.1'
    ]

    SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map.update({
        '.xhtml': 'application/xhtml+xml'
    })

    def setup(self):
        self.request.settimeout(self.timeout)
        SimpleHTTPServer.SimpleHTTPRequestHandler.setup(self)

    def parse_request(self):
        """ parse_request overridden """
        self.gzip_enable = False
        result = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self)
        try:
            accept_encoding = self.headers.get('Accept-Encoding')
            if accept_encoding and accept_encoding.find('gzip') != -1:
                self.gzip_enable = True
        except:
            pass
        return result

    def extract_query_param(self):
        """ extract_query_param added """
        query_string = urlparse.urlparse(self.path).query
        query_params = urlparse.parse_qs(query_string)
        for key, value in query_params.items():
            if len(value) == 1:
                query_params[key] = value[0]
        return query_params

    def extract_data(self, entity_body):
        """ extract_data added """
        data = {}
        content_type = self.headers.getheader('content-type')
        if content_type.startswith('application/json'):
            data = json.loads(entity_body)
            return data
        elif content_type.startswith('application/x-www-form-urlencoded'):
            data = urlparse.parse_qs(entity_body)
        else:
            raise ValueError('content-type value is invaild.')
        for key, value in data.items():
            if len(value) == 1:
                data[key] = value[0]
        return data

    def set_context(self):
        """ set context added """
        self.request_id = self.headers.get(settings.REQUEST_ID_HEADER, '')
        ctx.request = self

    def process_api(self):
        """ process_api added """
        if not self.path.startswith('/api/'):
            return False
        for prog, func in urls:
            result = prog.match(urlparse.urlparse(self.path).path[1:])
            if result:
                try:
                    if self.command in ['GET', 'HEAD', 'DELETE']:
                        self.query_params = self.extract_query_param()
                        self.body = ''
                        self.data = {}
                    elif self.command in ['POST', 'PUT']:
                        length = self.headers.getheader('content-length')
                        nbytes = int(length)
                        self.query_params = self.extract_query_param()
                        self.body = self.rfile.read(nbytes)
                        self.data = self.extract_data(self.body)
                except:
                    self.send_error(400)
                    return True
                try:
                    self.set_context()
                    response = func(self, **result.groupdict())
                    headers = response['headers']
                    content = response['content']
                    code = response['code']
                except:
                    logger.exception('internal server error')
                    self.send_error(500)
                    return True
                if code != 200:
                    self.send_custom(code, None, headers, content)
                    return True
                self.send_response(200)
                for key, value in headers.items():
                    self.send_header(key, value)
                content_type = headers.get('Content-Type')
                if content_type and self.gzip_enable and len(content) > 256:
                    gzip_types = ('text', 'javascript', 'json', 'xml')
                    for gzip_type in gzip_types:
                        if gzip_type in content_type:
                            content = self.gzip_encode(content)
                            self.send_header('Content-Encoding', 'gzip')
                            self.send_header('Vary', 'Accept-Encoding')
                            break
                self.send_header('Content-Length', str(len(content)))
                self.end_headers()
                if self.command != 'HEAD':
                    self.wfile.write(content)
                return True
        return False

    def do_GET(self):
        """ do_GET overridden """
        if not self.process_api():
            SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)

    def do_HEAD(self):
        """ do_HEAD overridden """
        if not self.process_api():
            SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self)

    def do_OPTIONS(self):
        """ do_OPTIONS added """
        self.send_response(200)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Accept, Authorization, Content-Type, X-Requested-With')
        self.send_header('Access-Control-Max-Age', '31536000')
        self.send_header('Content-Type', 'text/plain; charset=utf-8')
        self.send_header('Content-Length', '0')
        self.end_headers()

    def do_POST(self):
        """ do_POST added """
        if not self.process_api():
            path = self.translate_path(self.path)
            if os.path.exists(path):
                self.send_error(405)
            else:
                self.send_error(404)

    def do_PUT(self):
        """ do_PUT added """
        if not self.process_api():
            path = self.translate_path(self.path)
            if os.path.exists(path):
                self.send_error(405)
            else:
                self.send_error(404)

    def do_DELETE(self):
        """ do_DELETE added """
        if not self.process_api():
            path = self.translate_path(self.path)
            if os.path.exists(path):
                self.send_error(405)
            else:
                self.send_error(404)

    def gzip_encode(self, content):
        """ gzip_encode added """
        gz_stream = cStringIO.StringIO()
        gz_file = gzip.GzipFile(fileobj=gz_stream, mode='wb', compresslevel=2)
        gz_file.write(content)
        gz_file.close()
        return gz_stream.getvalue()

    def send_custom(self, code, message=None, headers=None, content=None):
        """ send_custom added """
        try:
            short, _ = self.responses[code]
        except KeyError:
            short, _ = '???', '???'
        if message is None:
            message = short
        self.send_response(code, message)
        if headers:
            for key, value in headers.items():
                self.send_header(key, value)
        if not content:
            if code in g_content:
                content = g_content[code]
                content_type = 'text/html; charset=utf-8'
            else:
                content = '%d, %s' % (code, message)
                content_type = 'text/plain; charset=utf-8'
            self.send_header('Content-Type', content_type)
        if code in (204, 304):
            self.send_header('Content-Length', '0')
        else:
            self.send_header('Content-Length', str(len(content)))
        self.end_headers()
        if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
            self.wfile.write(content)

    def send_response(self, code, message=None):
        """ send_response overridden """
        self.log_request(code)
        if message is None:
            if code in self.responses:
                message = self.responses[code][0]
            else:
                message = ''
        self.wfile.write('%s %d %s\r\n' % (self.protocol_version, code, message))
        self.response_headers = {}
        self.send_header('Server', self.version_string())
        self.send_header('Date', self.date_time_string())
        if code >= 400:
            self.send_header('Connection', 'close')
            self.close_connection = 1
        else:
            self.send_header('Connection', 'keep-alive')
            self.close_connection = 0

    def send_header(self, keyword, value):
        """ send_header overridden """
        self.response_headers[keyword] = value

    def end_headers(self):
        """ end_headers overridden"""
        for key in sorted(self.response_headers.keys()):
            value = self.response_headers[key]
            if isinstance(value, list):
                for each_val in value:
                    self.wfile.write('%s: %s\r\n' % (key, each_val))
            else:
                self.wfile.write('%s: %s\r\n' % (key, value))
        self.wfile.write('\r\n')

    def send_error(self, code, message=None):
        """ send_error overridden """
        self.send_custom(code, message)

    def translate_path(self, path):
        """ translate_path overridden """
        path = path.split('?', 1)[0]
        path = path.split('#', 1)[0]
        path = posixpath.normpath(urllib.unquote(path))
        words = path.split('/')
        words = filter(None, words)
        path = self.htdocs_path
        for word in words:
            drive, word = os.path.splitdrive(word)
            head, word = os.path.split(word)
            if word in (os.curdir, os.pardir):
                continue
            path = os.path.join(path, word)
        return path

    def address_string(self):
        """ address_string overridden """
        host, port = self.client_address[:2]
        return host

    def version_string(self):
        """ version_string overridden """
        return random.choice(self.server_names)

    def list_directory(self, path):
        """ list_directory overridden """
        self.send_error(403)


class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):

    """
    New features w/r to BaseHTTPServer.HTTPServer:
    - serves multiple requests simultaneously
    - catches socket.timeout and socket.error exceptions (raised from RequestHandler)
    """

    request_queue_size = 100
    allow_reuse_address = True
    daemon_threads = True

    def __init__(self, *args):
        BaseHTTPServer.HTTPServer.__init__(self, *args)

    def process_request_thread(self, request, client_address):
        """
        Overrides SocketServer.ThreadingMixIn.process_request_thread
        in order to catch socket.timeout
        """
        try:
            self.finish_request(request, client_address)
            self.close_request(request)
        except socket.timeout:
            sys.stderr.write('Timeout during processing of request from %s\n' % str(client_address))
        except:
            self.close_request(request)


def main():
    if len(sys.argv) == 3:
        ip = sys.argv[1]
        port = int(sys.argv[2])
    elif len(sys.argv) == 1:
        ip = '127.0.0.1'
        port = 8000
    else:
        print '%s <ip> <port>' % os.path.basename(sys.argv[0])
        sys.exit()

    httpd = ThreadedHTTPServer((ip, port), TimeoutHTTPRequestHandler)

    print 'Serving HTTP on %s port %d ...' % (ip, port)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print '^C received, shutting down server'


if __name__ == '__main__':
    main()
